package org.box.metadata.shell;

import java.io.IOException;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import org.box.metadata.cli.MetaCommandLineParser.ParserType;
import org.box.metadata.cli.annotation.Command;
import org.box.metadata.cli.exception.ParseException;
import org.box.metadata.cli.util.Util;
import org.box.metadata.shell.exception.TerminatedException;
import org.box.metadata.shell.exception.ValidationException;
import org.box.metadata.shell.message.ErrorMessageFormatter;
import org.box.metadata.shell.message.GreetingMessageFormatter;
import org.box.metadata.shell.message.HelpMessageFormatter;
import org.box.metadata.shell.message.InfoMessageFormatter;
import org.box.metadata.shell.message.MessageFormatters;
import org.box.metadata.shell.parser.ShellCLIParser;

/**
 * <p>
 * Provides second-level abstraction of Sell's API and implements fundamental
 * aspects for Shell's functionality such as handling User's requests and
 * processing exposed applications or/and commands.
 * </p>
 * 
 * 
 * @see Shell
 * 
 * @author <a href="mailto:alexander.box@gmail.com">Alexander Korobka</a>
 * 
 */
@SuppressWarnings("rawtypes")
public abstract class AbstractShell implements Shell {
	
	private final Set<ConfigurationHolder> holders = new LinkedHashSet<ConfigurationHolder>();

	private MessageFormatters messageFormatters = new MessageFormatters()
			.setMessageFormatter(new GreetingMessageFormatter())
			.setMessageFormatter(new ErrorMessageFormatter())
			.setMessageFormatter(new HelpMessageFormatter())
			.setMessageFormatter(new InfoMessageFormatter());
	
	private final Set<CommandHandlerListener> listeners = new CopyOnWriteArraySet<CommandHandlerListener>();
	
	private final ShellCLIParser cliParser;

	private boolean doExit;

	private final Class[] cfgClasses;

	protected AbstractShell(ParserType parserType, Class... cfgClasses) {
		
		this.cfgClasses = cfgClasses;
		for (Class c : cfgClasses) {
			holders.add(new ConfigurationHolder(c, parserType));
		}
		cliParser = new ShellCLIParser(parserType);
	}
	
	protected AbstractShell(Class... cfgClasses) {
		this(ParserType.GNU_COMMAND_SUPPORT_PARSER, cfgClasses);
	}
	
	/*
	 * (non-Javadoc)
	 * 
	 * @see org.box.metadata.shell.Shell#run(java.util.List, java.lang.String[])
	 */
	public void run(String[] args) {

		displayGreeting();

		handleUserInput(args);

		mainLoop();

	}

	/*
	 * (non-Javadoc)
	 * 
	 * @see
	 * org.box.metadata.shell.Shell#runAndExit(org.box.metadata.shell.Application
	 * , java.lang.String[])
	 */
	public void runAndExit(String[] args)
			throws TerminatedException {
		
		if (!executeCommand(args))
			displayHelp(null, null, cfgClasses);
	}

	@SuppressWarnings("unchecked")
	protected void runHandlers(Command c, Object cfg) throws ValidationException, TerminatedException {
		
		for (Class<? extends CommandHandler<?>> chc : c.handlers()) {
			CommandHandler handler = null;
			try {
				handler = chc.newInstance();
				fireHandlerStart(c, cfg, handler);
				handler.validate(cfg, this);
				handler.process(cfg, this);
				fireHandlerSuccess(c, cfg, handler);
			} catch (InstantiationException e) {
				throw new ValidationException(e);
			} catch (IllegalAccessException e) {
				throw new ValidationException(e);
			} 
		}
	}

	private void fireHandlerSuccess(Command c, Object cfg, CommandHandler handler) {
		for (CommandHandlerListener l : listeners) {
			l.onHandlerSuccess(handler, c, cfg);
		}
		
	}

	private void fireHandlerFailure(Command c, Object cfg, CommandHandler handler) {
		for (CommandHandlerListener l : listeners) {
			l.onHandlerFailure(handler, c, cfg);
		}
	}

	private void fireHandlerStart(Command c, Object cfg, CommandHandler handler) {
		for (CommandHandlerListener l : listeners) {
			l.onHandlerStart(handler, c, cfg);
		}
	}

	/**
	 * Method responsible to provide User's input
	 * 
	 * @return a string 
	 */
	protected abstract String getUserInput();

	/**
	 * Handles User's input and executes corresponding Shell's and User's
	 * application commands
	 * 
	 * @param handlersMap
	 * 
	 * @param args
	 *            - user's input as a list of arguments from the command line
	 */
	protected void handleUserInput(String[] args) {

		if (args.length > 0 && !executeCommand(args)) {
			displayInfo(String.format(
					"No appropriate command has been found for '%s'",
					args[0]));
		}
	}

	protected boolean executeCommand(String[] args) {
		for (ConfigurationHolder ch : holders) {
				Command c = cliParser.findCommandIn(ch.getCfgClass(), args);
				if (c != null) {
					Class<?> cmdCfg = Util.getCommandField(c, ch.getCfgClass()).getType();
					Object cfg = null;
					try {
						cfg = ch.parse(args);
						runHandlers(c, cfg);
						return true;
					} catch (ParseException e) {
						displayHelp(c, e, cmdCfg);
						fireHandlerFailure(c, cfg, null);
						return true;
					} catch (TerminatedException e) {
						displayError(c, e, cmdCfg);
						fireHandlerFailure(c, cfg, null);
						return true;
					} catch (ValidationException e) {
						displayHelp(c, e, cmdCfg);
						fireHandlerFailure(c, cfg, null);
						return true;
					}
				}
		}
		return false;
	}

	/**
	 * Runs the main loop
	 * 
	 * @param wrappers
	 * @throws IOException
	 */
	protected void mainLoop() {
		while (!doExit) {
			String[] args = getUserInput().split("\\s");
			handleUserInput(args);
		}
	}

	/*
	 * (non-Javadoc)
	 * @see org.box.metadata.shell.Shell#exit()
	 */
	public void exit() {
		doExit = true;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.box.metadata.shell.Shell#getCommands()
	 */
	public Set<Command> getCommands() {
		Set<Command> commands = new LinkedHashSet<Command>();
		commands.addAll(Util.createCLICommands(getCfgClasses()));
		return commands;
	}



	/**
	 * Prints a greeting message to the user on Shell's run
	 * @param cfgClass 
	 */
	protected void displayGreeting() {
		String msg = getFormatters().get(
				GreetingMessageFormatter.GREETING_MESSAGE_FORMATTER).format(
				null, null);
		
		displayMessage(msg);
	}

	/**
	 * Prints a help message about the given <code>application</code> usage to user
	 * @param e - an error from the application
	 * @param cfgClass 
	 * @param app - an application which usage help must be displayed
	 */
	protected void displayHelp(Command c, Throwable e, Class... cfgClass) {
		String msg = getFormatters().get(
				HelpMessageFormatter.HELP_MESSAGE_FORMATTER).format(
				c, cfgClass, e);
		
		displayMessage(msg);
	}
	
	/**
	 * Prints an error message from the given <code>application</code> to user
	 * @param e - the error from the application
	 * @param cfgClasses 
	 * @param app - an application which the error must be displayed
	 */
	protected void displayError(Command c, TerminatedException e, Class... cfgClasses) {
		String msg = getFormatters().get(
				ErrorMessageFormatter.ERROR_MESSAGE_FORMATTER).format(
				c, cfgClasses, e.getMessage());
		
		displayMessage(msg);
	}

	/**
	 * Prints an info message for user
	 * 
	 * @param message - a message to print to
	 */
	protected void displayInfo(String message) {
		String msg = getFormatters().get(
				InfoMessageFormatter.INFO_MESSAGE_FORMATTER).format(
				null, null, message);
		
		displayMessage(msg);

	}
	
	/*
	 * (non-Javadoc)
	 * @see org.box.metadata.shell.Shell#getCfgClasses()
	 */
	public Class[] getCfgClasses() {
		return cfgClasses;
	}

	/**
	 * The getter for message formatters' collection
	 * 
	 * @return a MessageFormatters object
	 */
	public MessageFormatters getFormatters() {
		return messageFormatters;
	}

	/**
	 * The setter for message formatters' collection
	 * 
	 * @param msgFormatters a MessageFormatters object
	 */
	public void setFormatters(MessageFormatters msgFormatters) {
		this.messageFormatters = msgFormatters;
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.box.metadata.shell.Shell#addListener(org.box.metadata.shell.CommandHandlerListener)
	 */
	public void addListener(CommandHandlerListener listener) {
		listeners.add(listener);
	}
	
	/*
	 * (non-Javadoc)
	 * @see org.box.metadata.shell.Shell#removeListener(org.box.metadata.shell.CommandHandlerListener)
	 */
	public void removeListener(CommandHandlerListener listener) {
		listeners.remove(listener);
	}
}
